package com.example.aspect;

import com.example.annotation.RepeatSubmit;
import com.example.constant.RedisKey;
import com.example.enums.BizCodeEnum;
import com.example.exception.BizException;
import com.example.interceptor.LoginInterceptor;
import com.example.utils.CommonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.aspectj.weaver.ast.Var;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author yinxin
 * @version 1.0
 * @Description:
 * @date 2022/6/1414:47
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {


    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * redission分布式锁;底层也是lua脚本
      */
    @Autowired
    RedissonClient redissonClient;

    /**
     * 定义 @Pointcut注解表达式，
     *  方式一：@annotation：当执行的方法上拥有指定的注解时生效（我们采用这）
     *  方式二：execution：一般用于指定方法的执行
     *
     * @param repeatSubmit
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit){
    }

    /**
     * 环绕通知, 围绕着方法执行
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     *
     * 方式一：单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以
     * 方式二：用@Pointcut和@Around联合注解也可以（我们采用这个）
     *
     *
     * 两种方式
     * 方式一：加锁 固定时间内不能重复提交
     * <p>
     * 方式二：先请求获取token，这边再删除token,删除成功则是第一次提交
     *
     * @param joinPoint
     * @param repeatSubmit
     * @return
     * @throws Throwable
     */
    @Around(value = "pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint,RepeatSubmit repeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();

        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        //用于记录成功过或者失败
        boolean res;
        String type = repeatSubmit.limitType().name();

        if(type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())){
            //type1: 参数形式防重提交
            long lockTime = repeatSubmit.lockTime();

            String ipAddr = CommonUtil.getIpAddr(request);
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            String className = method.getDeclaringClass().getName();
            String key = "order-server:repeat_submit:"+String.format("%s-%s-%s-%s", ipAddr, className, method, accountNo);

            log.info("分布式锁为:{}",key);
            //1、加锁（普通方式）
            //res = redisTemplate.opsForValue().setIfAbsent(key,"1",lockTime, TimeUnit.MILLISECONDS);

            //2、redisson分布式锁
            RLock lock = redissonClient.getLock("key");
            //加乐观锁-尝试加锁，最多等上0秒，上锁以后(默认5秒|自定义秒数)自动解锁
            res = lock.tryLock(0,lockTime,TimeUnit.SECONDS);
        }else {
            //type2: 令牌形式防重提交交
            String requestToken = request.getHeader("request-token");
            if(StringUtils.isBlank(requestToken)){
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
            }
            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);

            /**
             * 提交表单的token key
             * 方式一：不用lua脚本获取再判断，之前是因为 key组成是 order:submit:accountNo, value是对应的token，所以需要先获取值，再判断
             * 方式二：可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
             */
            res = redisTemplate.delete(key);
        }
        if(!res){
            log.error("请求重复提交");
            //这里要中断不然还会一直向下执行
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT);
        }

        log.info("环绕通知执行前");
        Object obj = joinPoint.proceed();
        log.info("环绕通知执行后");

        return obj;
    }
}
